/*******************************************************************************
 * Copyright (c) 2009 David Harrison.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.html 
 *  
 * Contributors:
 *     David Harrison - initial API and implementation
 ******************************************************************************/
package com.sfs.postcodes.dao;

import com.sfs.postcodes.beans.ConfigurationBean;
import com.sfs.postcodes.beans.GeocodeBean;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.StringTokenizer;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * The Class GeocoderDAOImpl.
 * 
 * @author David Harrison
 */

public class GeocoderDAOImpl implements GeocoderDAO {

    private Logger logger = Logger.getLogger(GeocoderDAO.class);
    
    private final String _encoding = "UTF-8";    
    
    private ConfigurationBean _configuration = null;
    
    
    /**
     * Sets the configuration bean for this GeocoderDAO instance.
     * 
     * @param configuration the new configuration
     */
    public void setConfiguration(final ConfigurationBean configuration)
    {
        this._configuration = configuration;
    }
    
    /**
     * Gets the configuration for this GeocoderDAO instance.
     * 
     * @return the configuration
     */
    protected ConfigurationBean getConfiguration()
    {
        return this._configuration;
    }
    
    /**
     * Perform a geocode lookup based on the supplied address, state and country
     * information. Returns a GeocodeBean if a valid code is found, if not null
     * is returned.
     * 
     * @param address the address to lookup
     * @param state the state the address is in
     * @param country the country where the address is in
     * @return a GeocodeBean containing the geocode if found, otherwise null
     * @throws GeocodeLookupException
     * @throws IOException
     */
    public GeocodeBean findGeocode(final String address, final String state,
            final String country) throws GeocodeLookupException, IOException
    {
        
        GeocodeBean geocode = null;
        
        ArrayList<String> addressArray = new ArrayList<String>();
        
        // Replace any instances of a carriage return with a comma
        String addressString = StringUtils.replace(address, "\n", ",");
                
        logger.debug("Address string: " + addressString);
                
        StringTokenizer st = new StringTokenizer(addressString,",");
        while (st.hasMoreTokens()) {
            String component = st.nextToken().trim();
            addressArray.add(component);
        }        
        
        logger.debug("Address array size: " + addressArray.size());
                
        for(int a = 0; a < addressArray.size(); a++) {

            // If a geocode has been found do not perform the search
            if(geocode==null) { 
            
                logger.debug("Geocode is null");

                StringBuffer sb = new StringBuffer();
                // Build search string
                for(int i = 0; i < addressArray.size(); i++) {
                        
                    final int key = i + a;
                    
                    if(key < addressArray.size()) {

                        if(sb.length()>0) {
                            sb.append(",");
                        }
                        String searchComponent = addressArray.get(key).trim();
                        searchComponent = StringUtils.replace(
                                searchComponent, "P.O. ", "PO ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "P.O.", "PO ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "Post Office ", "PO ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "R.D. ", "R D ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "R.D.", "R D ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "Rural Delivery ", "R D ");

                        sb.append(searchComponent);    

                        logger.debug("Search component: " + searchComponent);
                    }
                }
                if(StringUtils.isNotBlank(state)) {
                    sb.append(",");
                    sb.append(state);
                }
                if(StringUtils.isNotBlank(country)) {
                    sb.append(",");
                    sb.append(country);
                }

                // If the search is not an empty string perform the lookup
                if(sb.length()>0) {
                    geocode = this.performGeocodeLookup(sb.toString());
                }
            }
        }        
        return geocode;
    }
    
   
    /**
     * A private helper to perform the geocode lookup.
     * @param search the search string to query the database for
     * @return a GeocodeBean if a valid code is found, otherwise null
     * @throws GeocodeLookupException
     * @throws IOException
     */
    private GeocodeBean performGeocodeLookup(final String search) 
            throws GeocodeLookupException, IOException
    {
        GeocodeBean geocode = null;
        
        final String url = getConfiguration().getGeocodeUrl();
        final String key = getConfiguration().getGeocodeApiKey();
        final String keyParam = getConfiguration().getGeocodeKeyParam();
        final String addressParam = getConfiguration().getGeocodeAddressParam();
        
        URL geocodeURL = null;
        try {
            geocodeURL = new URL(url + "&" + addressParam + "=" 
                + URLEncoder.encode(search, this._encoding)
                + "&" + keyParam + "=" + key);
        } catch (MalformedURLException mue) {
            throw new GeocodeLookupException("Error constructing geocode URL: "
                    + mue.getMessage());
        } catch (UnsupportedEncodingException uee) {
            throw new GeocodeLookupException("Unsupported encoding of geocode: "
                    + uee.getMessage());
        }
                
        int statusCode = -1;
        
        BufferedReader in = new BufferedReader(
                new InputStreamReader(geocodeURL.openStream()));
        String line;
        while ((line = in.readLine()) != null) {            
            // Format: 200,6,42.730070,-73.690570
            statusCode = Integer.parseInt(line.substring(0, 3));
            if (statusCode == 200) {
                geocode = new GeocodeBean();
                geocode.setLatitude(
                        line.substring("200,6,".length(), 
                        line.indexOf(',', "200,6,".length())));
                geocode.setLongitude(
                        line.substring(line.indexOf(',', "200,6,".length()) + 1,
                        line.length()));
            }
        }            
        if (geocode == null) {
            switch (statusCode) {
                case 400:
                    throw new GeocodeLookupException("Bad Request");
                case 500:
                    throw new GeocodeLookupException(
                            "Unknown error from Google Encoder");
                case 601:
                    throw new GeocodeLookupException("Missing query");
                case 602:
                    return null;
                case 603:
                    throw new GeocodeLookupException("Legal problem");
                case 604:
                    throw new GeocodeLookupException("No route");
                case 610:
                    throw new GeocodeLookupException("Bad key");
                case 620:
                    throw new GeocodeLookupException("Too many queries");
            }
        }
        
        if(geocode!=null) {
            if(StringUtils.isBlank(geocode.getCoordinates())) {
                // No actual geocode found, return null
                geocode = null;
            }
        }        
        return geocode;
    }
    
}
